package top.hmtools.wxmp.httpclient;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.ByteArrayBody;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

import top.hmtools.base.StringTools;
import top.hmtools.wxmp.UrlServer.EUrlServer;
import top.hmtools.wxmp.accessToken.ATBean;
import top.hmtools.wxmp.accessToken.AccessTokenTools;
import top.hmtools.wxmp.configuration.ThreadLocalHander;
import top.hmtools.wxmp.configuration.WxmpConfiguration;
import top.hmtools.wxmp.exception.HttpRequestFailException;
import top.hmtools.wxmp.exception.WxApiRequestFailException;
import top.hmtools.wxmp.mediaAndMaterial.EMediaType;

/**
 * 基于httpclient封装的post、get、put、delete方法集合工具，每个请求均自动携带access Token
 * 
 * @author Hybomyth
 *
 */
public class HttpAdapter {

	private static Logger logger = LoggerFactory.getLogger(HttpAdapter.class);

	private static AccessTokenTools accessTokenTools;

	public static <T> T doGetJson(String uri, Object params, Class<T> clazz) {
		// 验证入参
		if (clazz == null) {
			throw new IllegalArgumentException("入参为空");
		}

		// 组装请求参数
		String urlStr = getUrlString(uri);
		String urlParamsStr = getUrlParamsStr(params);
		if (urlParamsStr != null && urlParamsStr.length() > 0) {
			urlStr = urlStr + "&" + urlParamsStr;
			if (logger.isDebugEnabled()) {
				logger.debug("完整的请求URL及参数原文是：{}", urlStr);
			}
		}

		// 组装get请求
		HttpGet httpGet = new HttpGet(urlStr);

		// 执行请求
		try {
			return doExecute(httpGet, getCharset()).toJavaObject(clazz);
		} catch (IOException e) {
			logger.error(e.getMessage(), e);
		} catch (WxApiRequestFailException e) {
			// 如果返回数据含有异常代码，且异常代码表示access Token 过期或者无效，则尝试重新刷新本地accessToken并再次获取请求微信接口获取数据
			T retryResult = doReTry(httpGet, clazz, e);
			if(retryResult!=null)return retryResult;
			throw e;
		}
		return null;
	}

	/**
	 * 上传输入流到指定微信公众号接口
	 * @param uri 要上传至的服务器接口地址
	 * @param formName	模拟form表单中的name属性值
	 * @param fileName	真实的文件名称
	 * @param inputStream	输入流
	 * @param mediaType	文件类型
	 * @param clazz	指定将返回的数据反序列化成的数据结构
	 * @return
	 */
	public static <T> T doPostStream(String uri, String formName, String fileName, InputStream inputStream,
			EMediaType mediaType, Class<T> clazz) {
		// 验证入参
		if (inputStream == null || clazz == null) {
			throw new IllegalArgumentException("入参为空");
		}

		// 组装post请求
		HttpPost httpPost = new HttpPost(getUrlString(uri) + "&type=" + mediaType.toString());

		byte[] byteArray = null;
		try {
			byteArray = IOUtils.toByteArray(inputStream);
			// 组装请求参数
			ByteArrayBody byteArrayBody = new ByteArrayBody(byteArray, fileName);
			HttpEntity reqEntity = MultipartEntityBuilder.create().addPart(formName, byteArrayBody).build();

			httpPost.setEntity(reqEntity);

			return doExecute(httpPost, getCharset()).toJavaObject(clazz);
		} catch (IOException e) {
			logger.error(e.getMessage(), e);
		}catch (WxApiRequestFailException e) {
			// 如果返回数据含有异常代码，且异常代码表示access Token 过期或者无效，则尝试重新刷新本地accessToken并再次获取请求微信接口获取数据
			T retryResult = doReTry(httpPost, clazz, e);
			if(retryResult!=null)return retryResult;
			throw e;
		}
		return null;
	}

	/**
	 * 执行post请求到微信公众号服务器获取数据
	 * 
	 * @param uri
	 *            资源地址，不需要带服务器域名/ip地址，本方法会自动补充。比如自定义菜单查询接口，只需传入
	 *            “cgi-bin/menu/get”即可
	 * @param params
	 *            参数的JavaBean实体类对象实例，本方法会自动转换成json格式
	 * @param charset
	 *            发送请求及接受反馈数据的字符编码格式，缺省utf-8
	 * @param clazz
	 * @return
	 */
	public static <T> T doPostJson(String uri, Object params, Class<T> clazz) {
		// 验证入参
		if (params == null || clazz == null) {
			throw new IllegalArgumentException("入参为空");
		}

		// 组装post请求
		HttpPost httpPost = new HttpPost(getUrlString(uri));

		// 组装请求参数
		try {
			String paramsJsonString = JSON.toJSONString(params);
			if (logger.isDebugEnabled()) {
				logger.debug("获取到请求微信接口参数内容原文是：{}", paramsJsonString);
			}
			httpPost.setEntity(
					new ByteArrayEntity(paramsJsonString.getBytes(getCharset()), ContentType.APPLICATION_JSON));
		} catch (UnsupportedEncodingException e) {
			throw new IllegalArgumentException(e);
		}

		try {
			return doExecute(httpPost, getCharset()).toJavaObject(clazz);
		} catch (IOException e) {
			logger.error(e.getMessage(), e);
		} catch (WxApiRequestFailException e) {
			// 如果返回数据含有异常代码，且异常代码表示access Token 过期或者无效，则尝试重新刷新本地accessToken并再次获取请求微信接口获取数据
			T retryResult = doReTry(httpPost, clazz, e);
			if(retryResult!=null)return retryResult;
			throw e;
		}
		return null;
	}
	
	/**
	 * 获取AccessTokenTools
	 * @return
	 */
	public static AccessTokenTools getAccessTokenTools(){
		// 获取配置信息
		WxmpConfiguration wxmpConfiguration = ThreadLocalHander.getWxmpConfiguration();
		if (wxmpConfiguration == null) {
			throw new IllegalArgumentException("获取配置信息对象实例异常：top.hmtools.wxmp.configuration.WxmpConfiguration");
		}

		// 获取微信服务器地址，缺省使用最近服务器
		EUrlServer eUrlServer = wxmpConfiguration.geteUrlServer();
		if (eUrlServer == null) {
			eUrlServer = EUrlServer.api;
		}
		if (accessTokenTools == null) {
			synchronized (HttpAdapter.class) {
				if (accessTokenTools == null) {
					accessTokenTools = AccessTokenTools.getInstance(eUrlServer);
				}
			}
		}
		return accessTokenTools;
	}
	
	/**
	 * 重试一次
	 * @param request
	 * @param clazz
	 * @param e
	 * @return
	 */
	private static <T>T doReTry(HttpRequestBase request,Class<T> clazz,WxApiRequestFailException e){
		ErrcodeBean errcodeBean = e.getErrcodeBean();
		// 如果是access Token异常，则刷新Token后重试一次
		if (40001 == errcodeBean.getErrcode() || 40014 == errcodeBean.getErrcode()
				|| 41001 == errcodeBean.getErrcode() || 42001 == errcodeBean.getErrcode()) {
			logger.info("access token 可能已过期，刷新一次本地缓存");
			accessTokenTools.getRefreshedAccessToken();
			try {
				String uriStr = request.getURI().toString();
				uriStr = replaceUriParamReg(uriStr, "access_token", accessTokenTools.getAccessToken());
				request.setURI(URI.create(uriStr));
				logger.info("access token 可能已过期，再重试请求一次");
				return doExecute(request, getCharset()).toJavaObject(clazz);
			} catch (IOException ee) {
				logger.error("重试从微信服务器获取数据时异常：" + ee.getMessage(), ee);
			}
		}
		return null;
	}
	
	/**
	 * 正则替换
	 * @param url
	 * @param paramName
	 * @param paramNewValue
	 * @return
	 */
	private static String replaceUriParamReg(String url, String paramName, String paramNewValue) {
		if (StringTools.isNotBlank(url,paramName,paramNewValue)) {
			url = url.replaceAll("(" + paramName + "=[^&]*)", paramName + "=" + paramNewValue);
		}
		return url;
	}


	/**
	 * 获取AccessTokenTools
	 * 
	 * @return
	 */
	private static ATBean getAccessToken() {
		// 获取配置信息
		WxmpConfiguration wxmpConfiguration = ThreadLocalHander.getWxmpConfiguration();
		if (wxmpConfiguration == null) {
			throw new IllegalArgumentException("获取配置信息对象实例异常：top.hmtools.wxmp.configuration.WxmpConfiguration");
		}

		// 获取微信服务器地址，缺省使用最近服务器
		EUrlServer eUrlServer = wxmpConfiguration.geteUrlServer();
		if (eUrlServer == null) {
			eUrlServer = EUrlServer.api;
		}

		//初始化accessTokenTools
		getAccessTokenTools();

		// 获取access Token
		ATBean atBean = accessTokenTools.getAccessTokenInfoBean(wxmpConfiguration.getAppid(),
				wxmpConfiguration.getAppsecret());
		if (logger.isDebugEnabled()) {
			logger.debug("当前获取的access token原文是：{}", atBean);
		}
		return atBean;
	}

	/**
	 * 获取字符编码，缺省使用UTF-8
	 * 
	 * @param charset
	 * @return
	 */
	private static String getCharset() {
		// 获取配置信息
		WxmpConfiguration wxmpConfiguration = ThreadLocalHander.getWxmpConfiguration();
		if (wxmpConfiguration == null) {
			throw new IllegalArgumentException("获取配置信息对象实例异常：top.hmtools.wxmp.configuration.WxmpConfiguration");
		}
		String charset = wxmpConfiguration.getCharset();
		if (charset == null || charset.trim().length() < 1) {
			charset = "UTF-8";
		}
		return charset;
	}

	/**
	 * 获取accessToken并与uri组装出请求URL
	 * 
	 * @param uri
	 * @return
	 */
	private static String getUrlString(String uri) {
		String result = "https://" + ThreadLocalHander.getWxmpConfiguration().geteUrlServer().toString() + "/" + uri
				+ "?access_token=" + getAccessToken().getAccess_token();
		if (logger.isDebugEnabled()) {
			logger.debug("当前请求的URL是：{}", result);
		}
		return result;
	}

	/**
	 * 将实体对象实例转换成 URL 参数字符串
	 * 
	 * @param paramsObj
	 * @return
	 */
	private static String getUrlParamsStr(Object paramsObj) {
		// 验证入参
		if (paramsObj == null) {
			return null;
		}

		// 先转换成 json map，然后遍历，这样写，其实是暂时偷懒不想自己写反射~~
		StringBuffer sbResult = new StringBuffer();
		JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(paramsObj));
		Set<Entry<String, Object>> entrySet = jsonObject.entrySet();
		Iterator<Entry<String, Object>> iterator = entrySet.iterator();
		while (iterator.hasNext()) {
			Entry<String, Object> item = iterator.next();
			sbResult.append(item.getKey() + "=" + item.getValue().toString());
			sbResult.append(iterator.hasNext() ? "&" : "");
		}

		return sbResult.toString();
	}

	/**
	 * 执行HTTP请求，并将成功获取的响应转换成json对象
	 * 
	 * @param request
	 * @param charset
	 * @return
	 * @throws ClientProtocolException
	 * @throws IOException
	 */
	private static JSONObject doExecute(HttpUriRequest request, String charset)
			throws ClientProtocolException, IOException {
		// 执行请求
		if(logger.isDebugEnabled()){
			logger.debug("发送的请求原文是：{}",request);
		}
		CloseableHttpResponse httpResponse = HttpClientHandleTools.getPoolHttpClient().execute(request);

		// 获取HTTP请求响应码
		StatusLine statusLine = httpResponse.getStatusLine();
		int statusCode = statusLine.getStatusCode();
		if (statusCode >= 400) {
			throw new HttpRequestFailException("请求微信服务器反馈异常：" + statusCode, statusCode);
		}

		// 获取响应body内容
		InputStream contentIS = httpResponse.getEntity().getContent();
		String contentJsonStr = IOUtils.toString(contentIS, charset);
		if (logger.isDebugEnabled()) {
			logger.debug("获取到微信反馈消息内容原文是：{}", contentJsonStr);
		}

		// 解析
		JSONObject jsonObject = JSON.parseObject(contentJsonStr);
		if (jsonObject.containsKey("errcode")) {// 可能微信侧执行异常
			int errcode = jsonObject.getIntValue("errcode");
			if (errcode == 0) {// 执行成功
				return jsonObject;
			} else {// 执行异常
				ErrcodeBean errcodeBean = jsonObject.toJavaObject(ErrcodeBean.class);
				throw new WxApiRequestFailException("请求微信公众号接口业务反馈异常", errcodeBean);
			}
		} else {// 可能是执行成功
			return jsonObject;
		}
	}
}
